# coding: utf-8
import collections
import copy
import math
import sys

epsilon = sys.float_info.epsilon

HANDLE_LINEAR = "Linear"
HANDLE_SPLINE = "Spline"
HANDLE_BEZIER = "Bezier"
HANDLE_BREAK = "Break"


def clamp(x, _min, _max):
    return max(_min, min(_max, x))


class Point(collections.namedtuple("Point", ["x", "y"])):
    __slots__ = ()

    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5

    def dist(self, other):
        return math.hypot(self.x - other.x, self.y - other.y)

    def clamp(self, _min, _max):
        if isinstance(_min, self.__class__) \
           and isinstance(_max, self.__class__):
            return Point(clamp(self.x, _min.x, _max.x),
                         clamp(self.y, _min.y, _max.y))
        else:
            return Point(clamp(self.x, _min, _max),
                         clamp(self.y, _min, _max))

    def __eq__(self, other):
        try:
            if self.x == other.x and self.y == other.y:
                return True
        except AttributeError:
            pass
        return False

    def __ne__(self,  other):
        return not self == other

    def __neg__(self):
        return Point(- self.x,  - self.y)

    def __add__(self, other):
        if isinstance(other, self.__class__):
            return Point(self.x+other.x, self.y+other.y)
        else:
            return Point(self.x+other, self.y+other)

    def __sub__(self, other):
        if isinstance(other, self.__class__):
            return Point(self.x-other.x, self.y-other.y)
        else:
            return Point(self.x-other, self.y-other)

    def __mul__(self, other):
        if isinstance(other, self.__class__):
            return Point(self.x*other.x, self.y*other.y)
        else:
            return Point(self.x*other, self.y*other)

    def __rmul__(self, other):
        return Point(self.x*other, self.y*other)

    def __rsub__(self, other):
        return Point(other - self.x, other - self.y)


class ControlPointModel:
    def __init__(self, mayaIndex=0,
                 handleType=HANDLE_LINEAR,
                 cp=(0, 0), h1=(0, 0), h2=(0, 0)):
        self.selected = False
        self.mayaIndex = mayaIndex
        self.handleType = handleType
        self.cp = Point(cp[0], cp[1])
        self.h1 = Point(h1[0], h1[1])
        self.h2 = Point(h2[0], h2[1])

    def __eq__(self, other):
        try:
            return self.mayaIndex == other.mayaIndex \
                and self.handleType == other.handleType \
                and self.cp == other.cp \
                and self.h1 == other.h1 \
                and self.h2 == other.h2
        except AttributeError:
            pass

        return False

    def __ne__(self, other):
        return not self == other

    @property
    def x(self):
        return self.cp.x

    @property
    def y(self):
        return self.cp.y

    def moveDelta(self, delta):
        self.cp = (self.cp + delta).clamp(0.0, 1.0)
        self.h1 = (self.h1 + delta).clamp(0.0, 1.0)
        self.h2 = (self.h2 + delta).clamp(0.0, 1.0)

    def getParams(self):
        if self.handleType == HANDLE_LINEAR:
            handleTypeNum = 0
        elif self.handleType == HANDLE_SPLINE:
            handleTypeNum = 1
        elif self.handleType == HANDLE_BEZIER:
            handleTypeNum = 2
        elif self.handleType == HANDLE_BREAK:
            handleTypeNum = 3
        else:
            handleTypeNum = 0

        return [handleTypeNum,
                self.h1.x, self.h1.y,
                self.cp.x, self.cp.y,
                self.h2.x, self.h2.y]

    def getMirror(self):
        if self.handleType == HANDLE_LINEAR:
            handleTypeNum = 0
        elif self.handleType == HANDLE_SPLINE:
            handleTypeNum = 1
        elif self.handleType == HANDLE_BEZIER:
            handleTypeNum = 2
        elif self.handleType == HANDLE_BREAK:
            handleTypeNum = 3
        else:
            handleTypeNum = 0

        return [handleTypeNum,
                1.0 - self.h2.x, self.h2.y,
                1.0 - self.cp.x, self.cp.y,
                1.0 - self.h1.x, self.h1.y]

    def getHandleSlopeIn(self):
        if self.handleType in {HANDLE_BEZIER, HANDLE_BREAK, HANDLE_SPLINE}:
            try:
                return (self.cp.y - self.h1.y) / (self.cp.x - self.h1.x)
            except ZeroDivisionError:
                return 0
        else:
            return 0

    def getHandleSlopeOut(self):
        if self.handleType in {HANDLE_BEZIER, HANDLE_BREAK, HANDLE_SPLINE}:
            try:
                return (self.h2.y - self.cp.y) / (self.h2.x - self.cp.x)
            except ZeroDivisionError:
                return 0
        else:
            return 0

    def getSlope(self, other):
        if not other:
            return 0
        try:
            return (self.cp.y - other.cp.y) / (self.cp.x - other.cp.x)
        except ZeroDivisionError:
            return 0

    def getModifiedHandleIn(self, other):
        if self.handleType == HANDLE_LINEAR:
            return Point(*self.cp)

        if not 0. < self.cp.x - self.h1.x:
            return Point(self.cp.x, self.h1.y)

        d = self.getHandleSlopeIn()
        if not other:
            x = min(self.h1.x, self.cp.x)
        else:
            x = clamp(self.h1.x, other.cp.x, self.cp.x-1e-9)

        return Point(x, self.cp.y + d*(x - self.cp.x))

    def getModifiedHandleOut(self, other):
        if self.handleType == HANDLE_LINEAR:
            return Point(*self.cp)

        if not 0. < self.h2.x - self.cp.x:
            return Point(self.cp.x, self.h2.y)

        d = self.getHandleSlopeOut()
        if not other:
            x = max(self.h2.x, self.cp.x)
        else:
            x = clamp(self.h2.x, self.cp.x+1e-9, other.cp.x)
        return Point(x, self.cp.y + d*(x - self.cp.x))

    def autoBezier(self, prev, fure):
        if self.handleType != HANDLE_SPLINE:
            return
        if prev.cp.x != fure.cp.x:
            slope = prev.getSlope(fure)
            x1 = 0.5 * (prev.cp.x - self.cp.x)
            self.h1 = Point(self.cp.x + x1, self.cp.y + slope*x1)
            x2 = 0.5 * (fure.cp.x - self.cp.x)
            self.h2 = Point(self.cp.x + x2, self.cp.y + slope*x2)
        else:
            self.h1 = 0.5 * (self.cp + prev.cp)
            self.h2 = 0.5 * (self.cp + fure.cp)

    def copy(self):
        return copy.deepcopy(self)


class ControlPointModels:
    def __init__(self, data=[]):
        self.__data = data

    # Returns a copy of the points data
    def getPointsData(self):
        return list(copy.deepcopy(self.__data))

    # Copies the data and sets it as internal points data
    def setPointsData(self, data):
        self.__data = list(copy.deepcopy(data))
        self.sort()

    def clear(self):
        self.__data = []

    def rowCount(self):
        return len(self.__data)

    def __iter__(self):
        for e in self.__data:
            yield e

    def iter(self):
        data = [None, ]
        data.extend([i for i in self.__data if not i.cp.x < 0])
        data.append(None)
        for i in range(1, len(data)-1):
            yield data[i], data[i-1], data[i+1]

    def addPoint(self, mayaIndex, params):
        # if mayaIndex < 0:
        #     mayaIndex = self.genMayaIndex()
        handleTypeNum = params[0]
        if handleTypeNum == 0:
            handleType = HANDLE_LINEAR
        elif handleTypeNum == 1:
            handleType = HANDLE_SPLINE
        elif handleTypeNum == 2:
            handleType = HANDLE_BEZIER
        elif handleTypeNum == 3:
            handleType = HANDLE_BREAK
        else:
            handleType = HANDLE_BEZIER

        cpm = ControlPointModel(mayaIndex=mayaIndex,
                                handleType=handleType,
                                h1=params[1:3],
                                cp=params[3:5],
                                h2=params[5:7])
        self.__data.append(cpm)
        self.sort()
        return cpm

    def remove(self, elem):
        if hasattr(elem, '__iter__'):
            for e in elem:
                self.remove(e)
            return

        if isinstance(elem, ControlPointModel):
            self.__data.remove(elem)
        if isinstance(elem, int):
            self.__data.remove(self.getPointbyMayaIndex(elem))

    def getPointbyMayaIndex(self, mid):
        for p in self.__data:
            if p.mayaIndex == mid:
                return p
        return None

    def sort(self):
        self.__data.sort(key=lambda p: p.cp.x)

    def genMayaIndex(self):
        mid = 1
        mids = [e.mayaIndex for e in self.__data]
        while mid in mids:
            mid += 1
        return mid

    def selection(self):
        for e in self.__data:
            if e.selected:
                yield e

    def deselect(self):
        for e in self.__data:
            e.selected = False


class SectionBase(object):
    def __init__(self, _start, _end):
        self.start = _start
        self.end = _end

    def getVal(self, s):
        return clamp((self.end - self.start) * s, self.start, self.end)

    def getSlope(self, s):
        return math.pi / 4. if self.start <= s <= self.end else 0.0


class SectionConst(SectionBase):
    def __init__(self, _s, _e, _v):
        super(SectionConst, self).__init__(_s, _e)
        self.v = _v

    def getVal(self, s):
        return self.v

    def getSlope(self, s):
        return 0.0


class SectionLinear (SectionBase):
    def __init__(self, _x0, _y0, _x1, _y1):
        super(SectionLinear, self).__init__(_x0, _x1)
        self.y0 = _y0
        self.y1 = _y1

    def getVal(self, x):
        try:
            s = (x - self.start) / (self.end - self.start)
        except ZeroDivisionError:
            s = 0
        return (1. - s)*self.y0 + s*self.y1

    def getSlope(self, s):
        if self.start <= s <= self.end \
                and ((self.y1 - self.y0) != 0 or (self.end - self.start) != 0):
            return math.atan2(self.y1 - self.y0, self.end - self.start)
        else:
            return 0.0


class SectionBezier(SectionBase):
    divc = 8

    def __init__(self, x0, y0, x1, y1, x2, y2, x3, y3):
        self.miles = []
        self.smiles = []
        self.mx = []
        self.my = []
        super(SectionBezier, self).__init__(x0, x3)
        self.mx = [- x0 + 3.0 * x1 - 3.0 * x2 + x3,
                   3.0*x0 - 6.0 * x1 + 3.0 * x2,
                   -3.0*x0 + 3.0 * x1,
                   x0]

        self.my = [-y0 + 3.0 * y1 - 3.0 * y2 + y3,
                   3.0 * y0 - 6.0 * y1 + 3.0 * y2,
                   -3.0 * y0 + 3.0 * y1,
                   y0]

        self.div()

    def div(self):
        for i in range(self.divc+1):
            p = Point(*self.Bezier(i*1.0/self.divc))
            self.miles.append(p)

            dx, dy = self.dBezier(i*1.0/self.divc)
            if dx != 0 or dy != 0:
                self.smiles.append(math.atan2(dy, dx))
            else:
                self.smiles.append(0)

    def getVal(self, x):
        i, r = self.getParameter(x)
        res = self.miles[i].y + r * (self.miles[i+1].y - self.miles[i].y)
        return res

    def getSlope(self, x):
        i, r = self.getParameter(x)
        res = self.smiles[i] + r * (self.smiles[i+1] - self.smiles[i])
        return res

    def Bezier(self, s):
        s2 = s*s
        s3 = s2*s
        x = self.mx[0] * s3 + self.mx[1] * s2 + self.mx[2] * s + self.mx[3]
        y = self.my[0] * s3 + self.my[1] * s2 + self.my[2] * s + self.my[3]
        return x, y

    def dBezier(self, s):
        dx = 3.0 * self.mx[0] * s * s + 2.0 * self.mx[1] * s + self.mx[2]
        dy = 3.0 * self.my[0] * s * s + 2.0 * self.my[1] * s + self.my[2]
        return dx, dy

    def dtest(self):
        return self.mx[1]*self.mx[1] - 3.0 * self.mx[0] * self.mx[2] < 0.0

    def getParameter(self, x):

        if x < self.miles[0].x:
            return 0, 0

        for i in range(self.divc):
            if self.miles[i+1].x < x:
                continue
            try:
                ratio = (x-self.miles[i].x) / (self.miles[i+1].x-self.miles[i].x)
            except ZeroDivisionError:
                ratio = 0
            return i, ratio

        return i, 1


class CCurve:

    def __init__(self):
        self.__sectionList = []

    def clear(self):
        self.__sectionList = []

    def rowCount(self):
        return len(self.__sectionList)

    def addNodeB(self, x0, y0, x1, y1, x2, y2, x3, y3):
        self.__sectionList.append(
            SectionBezier(x0, y0, x1, y1, x2, y2, x3, y3))

    def addNodeL(self, x0, y0, x1, y1):
        self.__sectionList.append(SectionLinear(x0, y0, x1, y1))

    def getValue(self, x):
        if not self.__sectionList:
            return 0

        for e in self.__sectionList:
            if e.end < x:
                continue

            res = e.getVal(x)
            break
        else:
            res = self.__sectionList[-1].getVal(self.__sectionList[-1].end)

        res = clamp(res, 0., 1.)
        return res

    def getSlope(self, x):
        if not self.__sectionList:
            return 0

        for e in self.__sectionList:
            if e.end < x:
                continue

            res = e.getSlope(x)
            break
        else:
            res = self.__sectionList[-1].getSlope(self.__sectionList[-1].end)

        return res
